package de.rj.urwalking.ui.activities;

import java.util.ArrayList;

import android.content.Context;
import android.content.Intent;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Geocoder;
import android.location.Location;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.maps.model.LatLng;
import com.slidinglayer.SlidingLayer;
import com.slidinglayer.SlidingLayer.OnInteractListener;
import com.threed.jpct.Logger;

import de.rj.urwalking.R;
import de.rj.urwalking.config.Config;
import de.rj.urwalking.config.Constants;
import de.rj.urwalking.data.Experiment;
import de.rj.urwalking.data.Poi;
import de.rj.urwalking.db.DBSalientObject;
import de.rj.urwalking.services.SensorLoggerService;
import de.rj.urwalking.tasks.ReverseGeocodingTask;
import de.rj.urwalking.tasks.SaveExperimentTask;
import de.rj.urwalking.ui.fragments.dialog.CancelExperimentDialog;
import de.rj.urwalking.util.DeviceUuidFactory;
import de.rj.urwalking.util.ExperimentManager;
import de.rj.urwalking.util.FileUpload;
import de.rj.urwalking.util.LocationUtil;
import de.rj.urwalking.util.PrefUtil;
import de.rj.urwalking.util.Util;
import de.rose_mobil.models.route.Route;

/**
 * Contains common stuff that is used in different activities like sensors, services, location.
 * 
 * @author rjj
 * 
 */

public abstract class URwalkingActivity extends SherlockFragmentActivity implements SensorEventListener,
		LocationListener, GooglePlayServicesClient.ConnectionCallbacks,
		GooglePlayServicesClient.OnConnectionFailedListener, LoaderCallbacks<Route> {

	protected static final String TAG = Config.TAG;
	protected static final boolean DEBUG = Config.DEBUG;
	
	public static final int TYPE_OUTDOOR = 1;
	public static final int TYPE_INDOOR = 2;
	public static final int TYPE_HYBRID = 3;
	public static final int TYPE_LEGO = 4;

	protected boolean useCompass = true;
	protected boolean mCompassIsAccurate;

	protected Sensor mSensorAccelerometer;
	protected Sensor mSensorMagneticField;
	protected Sensor mSensorRotationVector;
	protected Sensor mSensorLinearAcceleration;

	protected float[] accelData;
	protected float[] magnetData;
	protected float smoothingFactor = Config.COMPASS_SMOOTHING;
	protected float inR[] = new float[16];
	protected float outR[] = new float[16];
	private float mOrientation[] = new float[3];
	protected int displayRotation;
	protected float mOrientationZ;

	protected Intent mServiceIntent;
	private int experimentType;
	protected boolean mUsingRotationVector;
	protected boolean allowViewChange;
	private float mMagneticDeclinationInRadians;
//	private int cameraMode;

	private LocationRequest mLocationRequest;
	protected LocationClient mLocationClient;
	protected boolean followGPS = true;
	protected Location mLocation;

	// UI

	/** shows the geocoded location */
	protected TextView mLocationTextView;

	// sidebar elements
	protected TextView mDirectionsTextView;
	protected ImageView mDirectionsImageView;
	protected ListView landmarkListview;
	protected Button openLayerButton;
	protected SlidingLayer slidingLayer;

	/** a list of SOs that were loaded just recently and are displayed in the sidebar */
	protected ArrayList<DBSalientObject> latestLandmarks = new ArrayList<DBSalientObject>();
	/** the adapter used to display the latest SOs */
	protected ArrayAdapter<DBSalientObject> landmarkAdapter;

	protected Route mRoute;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		Logger.setLogLevel(Logger.LL_DEBUG);
		initPrefs();
		initGPS();
	}

	@Override
	protected void onStart() {
		super.onStart();
		mLocationClient.connect();
	}

	@Override
	protected void onStop() {
		super.onStop();
		// If the client is connected
		if (mLocationClient.isConnected()) {
			mLocationClient.removeLocationUpdates(this);
		}
		mLocationClient.disconnect();
	}

	@Override
	protected void onPause() {
		super.onPause();
		SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

		sensorManager.unregisterListener(this, mSensorAccelerometer);
		sensorManager.unregisterListener(this, mSensorMagneticField);

		if (mSensorRotationVector != null) {
			sensorManager.unregisterListener(this, mSensorRotationVector);
		}
		if (mSensorLinearAcceleration != null) {
			sensorManager.unregisterListener(this, mSensorLinearAcceleration);
		}

	}

	@Override
	protected void onResume() {
		super.onResume();
		SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

		sensorManager.registerListener(this, mSensorAccelerometer, SensorManager.SENSOR_DELAY_GAME);
		sensorManager.registerListener(this, mSensorMagneticField, SensorManager.SENSOR_DELAY_GAME);

		if (mSensorRotationVector != null) {
			sensorManager.registerListener(this, mSensorRotationVector, SensorManager.SENSOR_DELAY_GAME);
		}
		if (mSensorLinearAcceleration != null) {
			sensorManager.registerListener(this, mSensorLinearAcceleration, SensorManager.SENSOR_DELAY_GAME);
		}

	}

	/**
	 * Overrides the default behaviour of the back button and displays a confirmation dialog.
	 */
	@Override
	public void onBackPressed() {

		if (ExperimentManager.getInstance().isExperimentRunning()) {
			new CancelExperimentDialog().show(getSupportFragmentManager(), "CancelExperimentDialog");
			// try to send as much data as possible, in the end
			FileUpload.getInstance().finish();
		} else {
			// try to send as much data as possible, in the end
			FileUpload.getInstance().finish();
			super.onBackPressed();
		}

	}

	protected void initUI() {

		// keep screen always on
		getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

		mLocationTextView = (TextView) findViewById(R.id.locationTextView);
		initSidebar();
	}

	private void initGPS() {

		// check if we have a user defined location (test mode)
		if (PrefUtil.inTestMode(this) && !PrefUtil.readBoolean(R.string.key_pref_test_mode_actual_location, this, true)) {
			// build and use the test location
			String locationName = PrefUtil.readString(R.string.key_pref_example_location, this,
					getString(R.string.default_location));
			mLocation = LocationUtil.getLocationFor(locationName);
			// set flag to not use location updates later
			followGPS = false;
		}

		// enable location updates
		mLocationRequest = LocationRequest.create();
		mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
		mLocationRequest.setInterval(1000);
		mLocationClient = new LocationClient(this, this, this);

		//
		// // if we have still no location, just use a predefined one
		// if (currentBestLocation == null) {
		// Toast.makeText(this, "Konnte den aktuellen Standort nicht bestimmen", Toast.LENGTH_LONG).show();
		// currentBestLocation = LocationUtil.getLocationFor(getString(R.string.default_location));
		// }

	}

	private void initPrefs() {

		Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
		displayRotation = display.getRotation();

		allowViewChange = PrefUtil.readBoolean("allowViewChange", this, false);
		experimentType = PrefUtil.readInt("experimentType", this, Experiment.TYPE_OUTDOOR);

		PreferenceManager.setDefaultValues(this, R.xml.outdoor_preferences, false);

	}

	protected void checkPreferences() {

		if (PrefUtil.readBoolean(R.string.key_pref_use_gyro, this, true) && mSensorRotationVector != null) {
			mUsingRotationVector = true;
		} else {
			mUsingRotationVector = false;
		}

	}

	protected void initSensors() {
		SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

		mSensorAccelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
		mSensorMagneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

		if (sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) != null) {
			mSensorRotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
		}
		if (sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) != null) {
			mSensorLinearAcceleration = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION);
		}

		if (PrefUtil.readBoolean(R.string.key_pref_use_gyro, this, false)) {
			mUsingRotationVector = true;
		} else {
			mUsingRotationVector = false;
		}

		// using approximate coordinates to calculate magnetic declination
		// TODO: maybe use actual location if this is used far away from Regensburg
		GeomagneticField geoField = new GeomagneticField(Constants.REGENSBURG_LAT, Constants.REGENSBURG_LON,
				Constants.REGENSBURG_ALT, System.currentTimeMillis());
		mMagneticDeclinationInRadians = (float) (geoField.getDeclination() * Math.PI / 180f);

	}

	public void startExperiment() {
		Log.i(TAG, URwalkingActivity.class.getCanonicalName() + "Start experiment.");
		int participantId = PrefUtil.readParticipantId(this);
		DeviceUuidFactory duf = new DeviceUuidFactory(this);
		ExperimentManager.getInstance().newExperiment(participantId, 0, allowViewChange, experimentType,
				Util.getUrWalkingVersion(this), duf.getDeviceUuid().toString());

		String logFilename = ExperimentManager.getInstance().getCurrentExperiment().generateFilename();
		mServiceIntent = new Intent(this, SensorLoggerService.class);
		mServiceIntent.putExtra("logFilename", logFilename);
		startService(mServiceIntent);
		ActivityCompat.invalidateOptionsMenu(this);
	}

	public void finishExperiment(boolean saveExperiment, boolean finishAfterSaving) {
		if (mServiceIntent != null) {
			stopService(mServiceIntent);
		}

		if (saveExperiment) {
			new SaveExperimentTask(this, finishAfterSaving).execute();
		} else if (finishAfterSaving) {
			// Da ist der Wurm drin => Die Methode setzt Experiment = Null und
			// daher ist
			// im Cancel keine instanz verf�gbar
			ExperimentManager.getInstance().finishExperimentWithoutSaving();
			finish();
		}
		ActivityCompat.invalidateOptionsMenu(this);
	}

	@Override
	public void onAccuracyChanged(Sensor sensor, int accuracy) {

		// we only care about the sensor we are currently using
		if (!mUsingRotationVector && sensor.getType() != Sensor.TYPE_MAGNETIC_FIELD) return;
		if (mUsingRotationVector && sensor.getType() != Sensor.TYPE_ROTATION_VECTOR) return;

		if (accuracy <= SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM) {
			mCompassIsAccurate = false;
			ActivityCompat.invalidateOptionsMenu(this);
		} else {
			mCompassIsAccurate = true;
			ActivityCompat.invalidateOptionsMenu(this);

		}

	}

	@Override
	public void onSensorChanged(SensorEvent event) {

		if (!useCompass) return;

		if (mUsingRotationVector && event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
			SensorManager.getRotationMatrixFromVector(inR, event.values.clone());
			applyRotationMatrix();
		} else if (!mUsingRotationVector && event.sensor.getType() != Sensor.TYPE_ROTATION_VECTOR) {
			doOldCompassStuff(event);
		}

	}

	private void doOldCompassStuff(SensorEvent event) {
		if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
			accelData = Util.lowPass(event.values.clone(), accelData, smoothingFactor);
		}
		if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
			magnetData = Util.lowPass(event.values.clone(), magnetData, smoothingFactor);
		}

		if (accelData != null && magnetData != null) {

			if (SensorManager.getRotationMatrix(inR, null, accelData, magnetData)) {
				applyRotationMatrix();
			}
		}
	}

	private void applyRotationMatrix() {
		// http://stackoverflow.com/questions/8625804/compass-direction-is-different-depending-on-phone-orientation
		// Luckily, we are always in landscape mode. But we still have to take
		// care of different default rotations:
		switch (displayRotation) {
		// tablets that are in landscape mode by default (e.g. Acer Iconia A501)
		case Surface.ROTATION_0:
			SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_MINUS_X, outR);
			break;
		// tablets that are in portrait mode by default (e.g. Nexus 7)
		case Surface.ROTATION_90:
			outR = inR;
			break;
		// tablets that are in landscape mode by default (e.g. Acer Iconia A501)
		// upside down
		case Surface.ROTATION_180:
			SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_Y, SensorManager.AXIS_X, outR);
			break;
		// tablets that are in portrait mode by default (e.g. Nexus 7)
		// upside down
		case Surface.ROTATION_270:
			SensorManager.remapCoordinateSystem(inR, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y, outR);
			break;
		default:
			break;
		}
		SensorManager.getOrientation(outR, mOrientation);

		mOrientationZ = mOrientation[0] + mMagneticDeclinationInRadians;

		// mCompassView.updateDirection(mOrientation[0] +
		// mMagneticDeclinationInRadians);

	}

	@Override
	public void onConnectionFailed(ConnectionResult result) {
		if (DEBUG) Log.w(TAG, "error connecting to Google Play Services: " + result.toString());
	}

	@Override
	public void onConnected(Bundle arg0) {
		if (DEBUG) Log.d(TAG, "successfully connected to Google Play Services");
		mLocationClient.requestLocationUpdates(mLocationRequest, this);
	}

	@Override
	public void onDisconnected() {
		if (DEBUG) Log.d(TAG, "disconnected from Google Play Services");
	}

	public abstract void switchFragments(Fragment fragment);

	public boolean getUseCompass() {
		return useCompass;
	}

	public boolean getFollowGPS() {
		return followGPS;
	}

	protected void toggleGPS() {
		if (followGPS) {
			disableGpsFollowing(false);
		} else {
			enableGpsFollowing(false);
		}
	}

	public void enableGpsFollowing(boolean giveFeedback) {
		if (followGPS) return;

		followGPS = true;
		ActivityCompat.invalidateOptionsMenu(this);

		if (giveFeedback) {
			Toast.makeText(this, R.string.gps_activated, Toast.LENGTH_SHORT).show();
		}

	}

	public void disableGpsFollowing(boolean giveFeedback) {
		if (!followGPS) return;

		followGPS = false;
		ActivityCompat.invalidateOptionsMenu(this);

		if (giveFeedback) {
			Toast.makeText(this, R.string.gps_deactivated, Toast.LENGTH_SHORT).show();
		}
	}

	protected void toggleCompass() {
		if (useCompass) {
			disableCompassMovement(false);
		} else {
			enableCompassMovement(false);
		}
	}

	public void disableCompassMovement(boolean giveFeedback) {
		if (!useCompass) return;

		useCompass = false;
		ActivityCompat.invalidateOptionsMenu(this);

		if (giveFeedback) {
			Toast.makeText(this, R.string.compass_deactivated, Toast.LENGTH_SHORT).show();
		}
	}

	protected void enableCompassMovement(boolean giveFeedback) {
		if (useCompass) return;

		useCompass = true;
		ActivityCompat.invalidateOptionsMenu(this);

		if (giveFeedback) {
			Toast.makeText(this, R.string.compass_activated, Toast.LENGTH_SHORT).show();
		}
	}

	@Override
	public Loader<Route> onCreateLoader(int id, Bundle args) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void onLoadFinished(Loader<Route> loader, Route data) {
		// TODO Auto-generated method stub

	}

	@Override
	public void onLoaderReset(Loader<Route> loader) {
		// TODO Auto-generated method stub

	}

	public void updateLocationTextView() {
		// Bypass reverse-geocoding if the Geocoder service is not available on
		// the device.
		if (Geocoder.isPresent()) {
			// Since the geocoding API is synchronous and may take a while. You
			// don't want to lock up the UI thread. Invoking reverse geocoding in an AsyncTask.
			new ReverseGeocodingTask(this, mLocationTextView).execute(mLocation);
		}
	}

	protected void initSidebar() {
		slidingLayer = (SlidingLayer) findViewById(R.id.slidingLayer);
		slidingLayer.openLayer(true);

		openLayerButton = (Button) findViewById(R.id.openLayerButton);
		RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) openLayerButton.getLayoutParams();
		int margin = (int) TypedValue
				.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
		params.topMargin += margin;
		openLayerButton.setLayoutParams(params);

		openLayerButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View v) {
				Log.d(TAG, "clicked compass");
				if (slidingLayer.isOpened()) {
					slidingLayer.closeLayer(true);
				} else {
					slidingLayer.openLayer(true);

				}
			}
		});

		slidingLayer.setOnInteractListener(new OnInteractListener() {
			@Override
			public void onOpened() {
			}

			@Override
			public void onOpen() {
				openLayerButton.setVisibility(View.INVISIBLE);
			}

			@Override
			public void onClosed() {
			}

			@Override
			public void onClose() {
				openLayerButton.setVisibility(View.VISIBLE);
			}
		});

		mDirectionsTextView = (TextView) findViewById(R.id.directionsTextView);
		mDirectionsImageView = (ImageView) findViewById(R.id.directionsImageView);
		landmarkListview = (ListView) findViewById(R.id.landmarkListView);

		landmarkAdapter = new ArrayAdapter<DBSalientObject>(this, R.layout.row_landmark, R.id.nameTextView,
				latestLandmarks);
		landmarkListview.setAdapter(landmarkAdapter);

	}

	public void updateSideBar(ArrayList<DBSalientObject> salientObjects) {
		latestLandmarks.clear();
		latestLandmarks.addAll(salientObjects);
		landmarkAdapter.notifyDataSetChanged();
	}

}
